import fs from "fs";
import { isNumTypeSVT, IStructField, KBinary, KCoderVer, KWriteOp, StructCoder, StructValueType as SVT } from "k-coder";
import md5 from "md5";
import nodeXlsx from "node-xlsx";
import path from "path";


export class XlsxBuilder {

    private _xlsxs: IXlsx[] = [];
    private _maps: IMap[] = [];
    private _exportTyDic: { [ty: string]: boolean };

    constructor(
        /** 表格目录 */
        readonly xlsxDir: string,
        /** 数据输出目录 */
        readonly dataOutputDir: string,
        /** 代码输出目录 */
        readonly codeOutputDir: string,
        /** 导出类型 空为全导 */
        readonly exportTy?: string,
        /** 输出数据文件名 */
        readonly dataName = "data.bin",
        /** 输出代码文件名 */
        readonly codeName = "Xlsx",
        /** 输出代码命名空间 */
        readonly codeNameSpcae = "Xlsx",
        /** 输出表前缀名 */
        readonly prefix = "X",
    ) {
        if (exportTy) {
            this._exportTyDic = {};
            exportTy.split(/,| /).forEach(ty => this._exportTyDic[ty] = true);
        }
    }

    /** 构建表格对象 */
    build(onDone?: (time: number, size: number, count: number) => void) {
        const now = Date.now();
        this.loadAllXlsx(() => {
            this.sort();
            let size = 0;
            if (this.dataOutputDir) {
                size = this.buildData();
            }
            this.codeOutputDir && this.buildCode();
            onDone && onDone(Date.now() - now, size, this._xlsxs.length);
        });
    }

    /** 所有表格 */
    private loadAllXlsx(onDone: () => void) {
        //遍历表格目录读取表格
        console.log("===> 读取表格");
        const files = fs.readdirSync(this.xlsxDir);
        const defKeys: any = {
            name: 1,
            keys: 1,
            note: 2,
        };

        let count = files.length;
        files.forEach(fileName => {
            const match = fileName.match(/^([^~].*)\.xlsx$/);
            if (match) {
                const now = Date.now();
                const filePath = path.resolve(this.xlsxDir, fileName);
                fs.readFile(filePath, (err, data) => {
                    console.log(filePath);
                    if (err) {
                        console.log(`${fileName}读取失败！`);
                        fileDone();
                        return;
                    }
                    const sheets = nodeXlsx.parse(data);
                    let count = sheets.length;
                    sheets.forEach(sheet => {
                        const rows = sheet.data;
                        const defDic: { [key: string]: string } = {};
                        const defVal = rows[1];
                        rows[0].forEach((val, col) => {
                            if (defKeys[val]) {
                                defDic[val] = defVal[col];
                            }
                        });
                        for (const key in defKeys) {
                            if (defKeys[key] === 1 && !defDic[key]) {
                                //非表格
                                sheetDone();
                                return;
                            }
                        }

                        const tableName = defDic["name"];
                        const tableKeys = defDic["keys"];
                        const tableNote = defDic["note"];

                        //检查命名是否规范
                        assert(!tableName.match(/[^_0-9a-zA-Z]/), `表格名称[${tableName}]命名不规范`);
                        console.log(">>> " + tableName);

                        this.loadXlsx(rows, fileName, tableName, tableKeys, tableNote);
                        sheetDone();
                    });

                    function sheetDone() {
                        if (--count === 0) {
                            console.log(`耗时：${(Date.now() - now) / 1000}s`);
                            fileDone();
                        }
                    }
                });
            } else {
                fileDone();
            }
        })

        function fileDone() {
            if (--count === 0) {
                onDone();
            }
        }
    }

    /** 排序 */
    private sort() {
        this._xlsxs.sort((a, b) => a.name.localeCompare(b.name));
        this._maps.sort((a, b) => a.tableName.localeCompare(b.tableName));
    }

    /** 读取单个表格 */
    private loadXlsx(rows: any[][], xlsxName: string, tableName: string, tableKeys: string, tableNote: string) {

        const xlsx: IXlsx = {
            note: `[${xlsxName} -> ${tableName}] ${tableNote}`,
            name: tableName,
            isMisc: false,
            keys: null,
            keysTypeStr: null,
            fields: [],
            data: []
        }, maps: IMap[] = [];

        const keyDic: { [key: string]: string } = {};
        if (tableKeys === "#") {
            xlsx.isMisc = true;
            //将misc类型转成与普通表格一致的结构再统一处理
            const temp: any[][] = [];
            for (let i = 0; i < rows.length; ++i) {
                if (i < 3) {
                    temp.push(rows[i]);
                } else if (i === 3 || rows[i][0] === "*") {
                    for (let j = 1; j < rows[i].length; j++) {
                        if (i === 3) {
                            temp[j + 2] = [];
                        }
                        temp[j + 2].push(rows[i][j]);
                    }
                }
            }
            temp[8][0] = "*";
            rows = temp;
        } else {
            assert(tableKeys, `[${tableKeys}] keys 不能为空`);
            xlsx.keys = tableKeys.split(",").map(key => keyDic[key] = key);
            xlsx.keysTypeStr = [];
        }
        for (let row = 8; row < rows.length; row++) {
            //第一列有*号标识才导出
            const vals = rows[row];
            if (vals[0] === "*") {
                xlsx.data.push({ row: row + 1, vals });
            }
        }


        //导出类型 字段注释 字段名称 数据类型
        let row = 3;
        const exportTys = rows[row++] as string[];
        const notes = rows[row++] as string[];
        const fields = rows[row++] as string[];
        const types = rows[row++] as string[];

        if (!xlsx.isMisc) {
            const checkKey = Object.assign({}, keyDic);
            fields.forEach(field => delete checkKey[field]);
            assert(!Object.keys(checkKey).length, `请检查keys[${tableKeys}]是否有误`);
        }

        //根据导出类型，筛选出需要导出的字段
        for (let col = 1; col < exportTys.length; col++) {
            const exportTy = exportTys[col];
            if (!exportTy || (this.exportTy && !exportTy.split(",").some(ty => this._exportTyDic[ty]))) continue;
            const field = fields[col];
            assert(!field.match(/[^_0-9a-zA-Z]/), `字段[${field}]命名不规范`);
            const type = types[col];
            const match = type.match(/([ _0-9a-zA-Z]+)(?:\(([,_0-9a-zA-Z]+)\))?((?:\[\])+)?(?:<(.+)>)?/);
            assert(match, `字段[${field}]数据类型<${type}>错误`);
            const [_, valTy, valParam, arrStr = "", customTsType] = match;
            assert(type === valTy + (valParam ? `(${valParam})` : "") + arrStr + (customTsType ? `<${customTsType}>` : ""), `字段[${field}]数据类型<${type}>错误`);

            const note = notes[col];

            if (valTy.match(/(map|enum|const enum)/)) {
                // === 解析映射
                const split = valParam.split(",");;
                assert(split.length >= 2 && split.length <= 3, `数据类型<${type}>参数错误`);
                const [keyField, valField, noteField] = split;
                let map: IMap = {
                    tableName,
                    mapType: valTy,
                    note: `[${xlsxName} -> ${tableName}] ${note}`,
                    name: field,
                    data: xlsx.data,
                    noteCol: -1,
                    keyCol: -1,
                    valCol: -1,
                }
                fields.some((field, col) => {
                    if (field === keyField) {
                        map.keyCol = col;
                    }
                    if (field === valField) {
                        map.valCol = col;
                    }
                    if (noteField && field === noteField) {
                        map.noteCol = col;
                    }
                    return map.keyCol > -1 && map.valCol > -1 && (!noteField || map.noteCol > -1);
                })
                assert(map.keyCol > -1, `[${field}]映射<${type}>找不到<${keyField}>字段`);
                assert(map.valCol > -1, `[${field}]映射<${type}>找不到<${valField}>字段`);
                assert(!noteField || map.noteCol > -1, `[${field}]映射<${type}>找不到<${noteField}>字段`);
                maps.push(map);
            } else {
                // === 解析字段
                let svt: SVT = (SVT as any)[valTy];

                const param = valParam ? parseInt(valParam) : 0;
                assert(!isNaN(param), `数据类型<${type}>参数错误`);

                if (valTy === "float" && !param) {
                    svt = SVT.float32;
                }

                if (svt === undefined) {
                    if (valTy === "int" || valTy === "float") {
                        let sign = false;
                        let long = false;
                        function checkNum(val: number | any[]) {
                            if (sign && long) {
                                return true;
                            }

                            if (typeof val === "number") {
                                if (val < 0) {
                                    sign = true;
                                    if (val < -2147483648) {
                                        long = true;
                                    }
                                }
                                if (val > 2147483647) {
                                    long = true;
                                    console.log("checkNum", val);
                                }
                            } else if (val instanceof Array) {
                                val.some(val => checkNum(val));
                            } else if (val) {
                                try {
                                    const nums = JSON.parse(val);
                                    return checkNum(nums);
                                } catch {
                                    assert(false, `字段[${field}]数据解析失败`);
                                }

                                return checkNum(JSON.parse(val));
                            }

                            return sign && long;
                        }
                        xlsx.data.some(data => {
                            const val = data.vals[col];
                            return checkNum(val);
                        });
                        if (valTy === "int") {
                            if (sign) {
                                svt = long ? SVT.aint64 : SVT.aint32;
                            } else {
                                svt = long ? SVT.auint64 : SVT.auint32;
                            }
                        } else {
                            if (sign) {
                                svt = long ? SVT.floatAint64 : SVT.floatAint32;
                            } else {
                                svt = long ? SVT.floatAuint64 : SVT.floatAuint32;
                            }
                        }
                    }
                }
                assert(svt !== undefined, `字段[${field}]数据类型<${type}>错误`);

                let tsType = ""
                switch (svt) {
                    case SVT.bool:
                        tsType = "boolean";
                        break;
                    case SVT.uint8:
                    case SVT.uint16:
                    case SVT.uint32:
                    case SVT.uint64:
                    case SVT.auint32:
                    case SVT.auint64:
                    case SVT.int8:
                    case SVT.int16:
                    case SVT.int32:
                    case SVT.int64:
                    case SVT.aint32:
                    case SVT.aint64:
                    case SVT.floatStr:
                        tsType = "number";
                        break;
                    case SVT.floatAuint32:
                    case SVT.floatAuint64:
                    case SVT.floatAint32:
                    case SVT.floatAint64:
                        assert(param > 0, `字段[${field}]数据类型<${type}>参数错误`);
                        tsType = "number";
                        break;
                    case SVT.str:
                        tsType = "string";
                        break;
                    case SVT.obj:
                        tsType = `Readonly<${customTsType || "{ [key: string]: any }"}>`;
                        break;
                    case SVT.arr:
                        tsType = `readonly ${customTsType || "any[]"}`;
                        break;
                }
                assert(tsType, `字段[${field}]数据类型<${type}>错误`);

                const arrVec = Math.floor(arrStr.length / 2);
                const isKey = Boolean(keyDic[field]);
                if (isKey) {
                    assert(arrVec === 0 && ["number", "string"].includes(tsType), `作为key的字段[${field}]的数据类型必须为数字或字符串类型`)
                    xlsx.keysTypeStr[xlsx.keys.indexOf(field)] = tsType;
                }

                xlsx.fields.push({
                    name: field,
                    col,
                    note,
                    type: tsType,
                    svt,
                    arrVec,
                    structField: [field, svt, arrVec, param],
                    isKey
                });
            }
        }

        if (xlsx.fields.length > 0) {
            this._xlsxs.push(xlsx);
        }
        if (maps.length > 0) {
            this._maps = this._maps.concat(maps);
        }
    }

    private buildData() {
        console.log("===> 构建数据");
        const now = Date.now();
        const b = new KBinary();

        this._xlsxs.forEach(xlsx => {

            //先处理数据，判断有无错误
            let md5Str = "";
            const keyCheck: { [key: string]: number } = {};
            const rows = xlsx.data.map(data => {
                const obj: any = {};
                const keys: any[] = [];
                xlsx.fields.forEach(field => {
                    let val = data.vals[field.col];
                    if (val == undefined || val === "") {
                        assert(xlsx.isMisc || !field.isKey, `(行${data.row})[${field.name}]作为key的字段不能为空`);
                        return;
                    }
                    md5Str += val;

                    const assertStr = `(行${data.row})[${field.name}]字段数据解析失败 --- ${val}`;
                    if (field.arrVec > 0 || field.svt >= SVT.any) {
                        try {
                            val = JSON.parse(val);
                        } catch {
                            assert(false, assertStr + " JSON.parse 转化失败");
                        }
                    } else {
                        if (isNumTypeSVT(field.svt)) {
                            val = Number(val);
                            assert(!isNaN(val), assertStr + "非数字");
                        } else if (field.svt === SVT.str) {
                            val = String(val).replace(/\\\\n/g, "\n");
                        }
                    }

                    if (field.isKey) {
                        keys[xlsx.keys.indexOf(field.name)] = val;
                    }

                    obj[field.name] = val;
                });
                const key = keys.join("-");
                assert(!keyCheck[key], `(行${keyCheck[key]}) <-> (行${data.row}) keys<${key}>冲突`);
                keyCheck[key] = data.row;

                return obj;
            });

            //[写入] >>> str-表名 str-8位md5 uint8-是否misc
            b.wStr(xlsx.name);
            b.wStr(md5(md5Str).slice(0, 8));
            b.wUint8(xlsx.isMisc ? 1 : 0);

            //记录起始长度 用于计算当前表数据长度
            const startLen = b.wLen;
            const tableLenOp = b.wAddOp(KWriteOp.auint32, 0, 0);


            if (!xlsx.isMisc) {
                //[写入] >>> uint8-keys字段数量
                b.wUint8(xlsx.keys.length);
                xlsx.keys.forEach(key => {
                    //[写入] >>> str-key字段名
                    b.wStr(key);
                });
            }

            //[写入] >>> auint32-字段数量
            b.wAuint32(xlsx.fields.length);

            //[写入] >>> 字段结构
            const struct: IStructField[] = [];
            xlsx.fields.forEach(field => {
                //[写入] >>> 字段名
                const [fieldName, svt, arrVec, param] = field.structField;

                //[写入] >>> 数据结构 [str-字段名，uint8-值类型 uint8-数据维度 uint8-float特殊参数]
                b.wStr(fieldName);
                b.wUint8(svt);
                b.wUint8(arrVec);
                b.wUint8(param as number);

                const fst = field.structField.concat() as IStructField;
                if (!field.isKey) {
                    fst[0] += "?";
                }
                struct.push(fst);
            });

            //记录起始长度 用于计算行数据长度
            const rowStartLen = b.wLen;
            const rowLenOp = xlsx.isMisc ? null : b.wAddOp(KWriteOp.auint32, 0, 0);

            //[写入] >>> 数据行数
            !xlsx.isMisc && b.wAuint32(rows.length);

            //[写入] >>> 字段数据
            const valCoder = new StructCoder(struct);
            rows.forEach(row => valCoder.encode(row, b));
            if (rowLenOp) {
                //[写入] >>> auint32-行数据长度
                const rowLen = b.wLen - rowStartLen;
                rowLenOp.val = rowLen;
                b.wLen += b.getUintLen(rowLen);
            }

            //[写入] >>> auint32-表数据长度
            const tabelLen = b.wLen - startLen;
            tableLenOp.val = tabelLen;
            b.wLen += b.getUintLen(tabelLen);
        });

        //输出data文件
        const data = b.wEnd();
        fs.writeFileSync(path.resolve(this.dataOutputDir, `${this.dataName}`), data, { flag: "w+" });
        console.log(`耗时：${(Date.now() - now) / 1000}s`);

        return data.length;
    }

    private buildCode() {
        console.log("===> 构建代码");
        const now = Date.now();
        let code = "const win: any = (globalThis as any) || (window as any);\n";
        code += `/** \n`;
        code += ` * 表格数据\n`;
        code += ` * (工具生成，请不要手动修改...)\n`;
        code += ` */\n`;
        code += `export namespace ${this.codeNameSpcae} {\n`;
        code += `\n`;
        code += `    let b: any, Coder: any;\n`;
        code += `    /** 传入k-coder来进行初始化 */\n`;
        code += `    export function init(kcoder: { KCoderVer: any, KBinary: any, StructCoder: any }) {\n`;
        code += `        const { KCoderVer, KBinary, StructCoder } = kcoder;\n`;
        code += `        if (!KCoderVer.checkVer("${KCoderVer.ver}")) {\n`;
        code += `            throw new Error("表格数据初始化失败, kcoder版本号不同 need:${KCoderVer.ver} cur:" + KCoderVer.ver);\n`;
        code += `        }\n`;
        code += `        b = new KBinary();\n`;
        code += `        Coder = StructCoder;\n`;
        code += `    }\n`;
        code += `\n`;
        code += `    /** 表数量 */\n`;
        code += `    export const count = ${this._xlsxs.length};\n`;
        code += `    /** 已加载的表 */\n`;
        code += `    export const loadedDic: any = {};\n`;
        code += `    const dataDic: any = {};\n`;
        code += `    const prefix = "${this.prefix}";\n`;
        code += `\n`;
        code += `    /** 读取表数据 */\n`;
        code += `    export function load(buf: Uint8Array, onCount?: (count: number) => void, onReload?: (reloadDic: { [tableName: string]: any }) => void) {\n`;
        code += `        let c = 0;\n`;
        code += `        b.rStart(buf);\n`;
        code += `        const reloadDic: any = Object.assign({}, loadedDic);\n`;
        code += `        while (b.rHasNext) {\n`;
        code += `            const tableName = b.rStr();\n`;
        code += `            const md5 = b.rStr();\n`;
        code += `            const isMisc = b.rUint8();\n`;
        code += `            const data = b.rCut(b.rAuint32());\n`;
        code += `            const loaded = loadedDic[tableName];\n`;
        code += `            if (loaded && loaded.$md5 === md5) {\n`;
        code += `                delete reloadDic[tableName];\n`;
        code += `            } else {\n`;
        code += `                dataDic[tableName] = data;\n`;
        code += `                const clsName = prefix + tableName;\n`;
        code += `                Object.defineProperty(win, clsName, {\n`;
        code += `                    get: () => loadedDic[tableName] = (Object.defineProperty(win, clsName, {\n`;
        code += `                        value: loadOne(clsName, tableName, md5, isMisc),\n`;
        code += `                        enumerable: true,\n`;
        code += `                        configurable: true\n`;
        code += `                    }) as any)[clsName],\n`;
        code += `                    enumerable: true,\n`;
        code += `                    configurable: true\n`;
        code += `                });\n`;
        code += `            }\n`;
        code += `            onCount && onCount(++c);\n`;
        code += `        }\n`;
        code += `        for (const name in reloadDic) {\n`;
        code += `            win[prefix + name];\n`;
        code += `        }\n`;
        code += `        b.rEnd();\n`;
        code += `        onReload && onReload(reloadDic);\n`;
        code += `    }\n`;
        code += `\n`;
        code += `    const clsDic: any = {};\n`;
        code += `    function loadOne(clsName: string, tableName: string, md5: string, isMisc: number) {\n`;
        code += `        b.rStart(dataDic[tableName]);\n`;
        code += `        delete dataDic[tableName];\n`;
        code += `\n`;
        code += `        const keyDic: any = {};\n`;
        code += `        const keyFields: string[] = [];\n`;
        code += `        if (!isMisc) {\n`;
        code += `            const keyLen = b.rUint8();\n`;
        code += `            for (let i = keyLen; --i >= 0;) {\n`;
        code += `                const key = b.rStr();\n`;
        code += `                keyDic[key] = true;\n`;
        code += `                keyFields.push(key);\n`;
        code += `            }\n`;
        code += `        }\n`;
        code += `\n`;
        code += `        const fieldLen = b.rAuint32();\n`;
        code += `        const struct: any[] = [];\n`;
        code += `        for (let i = fieldLen; --i >= 0;) {\n`;
        code += `            const fieldName = b.rStr();\n`;
        code += `            const svt = b.rUint8();\n`;
        code += `            const arrVec = b.rUint8();\n`;
        code += `            const param = b.rUint8();\n`;
        code += `            struct.push([keyDic[fieldName] ? fieldName : fieldName + "?", svt, arrVec, param]);\n`;
        code += `        }\n`;
        code += `        const coder = new Coder(struct);\n`;
        code += `\n`;
        code += `        if (isMisc) {\n`;
        code += `            const misc = coder.decode(b, loadedDic[tableName]);\n`;
        code += `            misc.$name = tableName;\n`;
        code += `            misc.$md5 = md5;\n`;
        code += `            return misc;\n`;
        code += `        }\n`;
        code += `\n`;
        code += `        const Cls = clsDic[tableName] ||= function () { };\n`;
        code += `        Object.defineProperty(Cls, "name", { value: clsName, enumerable: true, configurable: true });\n`;
        code += `        Cls.$name = tableName;\n`;
        code += `        Cls.$md5 = md5;\n`;
        code += `        Cls.rowData = b.rCut(b.rAuint32());\n`;
        code += `        const getRows = () => {\n`;
        code += `            b.rStart(Cls.rowData);\n`;
        code += `            const rows: any[] = [];\n`;
        code += `            const rowLen = b.rAuint32();\n`;
        code += `            for (let i = 0; i < rowLen; ++i) {\n`;
        code += `                const row = coder.decode(b, new Cls());\n`;
        code += `                let dic = Cls.rowDic;\n`;
        code += `                for (let j = 0, len = keyFields.length - 1; j < len; ++j) {\n`;
        code += `                    const key = row[keyFields[j]];\n`;
        code += `                    dic = (dic[key] ||= {});\n`;
        code += `                }\n`;
        code += `                const key = row[keyFields[keyFields.length - 1]];\n`;
        code += `\n`;
        code += `                if (dic[key]) {\n`;
        code += `                    for (let k in row) {\n`;
        code += `                        dic[key][k] = row[k];\n`;
        code += `                    }\n`;
        code += `                    rows.push(dic[key]);\n`;
        code += `                } else {\n`;
        code += `                    dic[key] = row;\n`;
        code += `                    rows.push(row);\n`;
        code += `                }\n`;
        code += `            }\n`;
        code += `\n`;
        code += `            b.rEnd();\n`;
        code += `            delete Cls.rowData;\n`;
        code += `            Object.defineProperty(Cls, "rows", { value: rows, enumerable: true, configurable: true });\n`;
        code += `            return rows;\n`;
        code += `        }\n`;
        code += `        if (Cls.rowDic) {\n`;
        code += `            getRows();\n`;
        code += `        } else {\n`;
        code += `            Cls.rowDic = {};\n`;
        code += `            Object.defineProperty(Cls, "rows", { get: getRows, enumerable: true, configurable: true });\n`;
        code += `        }\n`;
        code += `\n`;
        code += `        const rowDic = Cls.rowDic;\n`;
        code += `        Cls.get = (...keys: any[]) => {\n`;
        code += `            Cls.rows;\n`;
        code += `            switch (keys.length) {\n`;
        code += `                case 1: Cls.get = (k1: any) => rowDic[k1]; break;\n`;
        code += `                case 2: Cls.get = (k1: any, k2: any) => (rowDic[k1] && rowDic[k1][k2]); break;\n`;
        code += `                case 3: Cls.get = (k1: any, k2: any, k3: any) => (rowDic[k1] && rowDic[k1][k2] && rowDic[k1][k2][k3]); break;\n`;
        code += `                default:\n`;
        code += `                    Cls.get = (...keys: any[]) => {\n`;
        code += `                        let dic = rowDic;\n`;
        code += `                        for (let i = 0; i < keys.length; ++i) {\n`;
        code += `                            const key = keys[i];\n`;
        code += `                            if (i === keys.length - 1) {\n`;
        code += `                                return dic[key];\n`;
        code += `                            } else if (dic[key]) {\n`;
        code += `                                dic = dic[key];\n`;
        code += `                            }\n`;
        code += `                            return undefined;\n`;
        code += `                        }\n`;
        code += `                    };\n`;
        code += `            }\n`;
        code += `            return Cls.get(...keys);\n`;
        code += `        }\n`;
        code += `\n`;
        code += `        return Cls;\n`;
        code += `    }\n`;
        code += `}\n`;

        this._maps.forEach(map => {

            switch (map.mapType) {
                case "map":
                    code += "\n";
                    code += `/** ${map.note} */\n`;
                    code += `const _${map.name} = win["${map.name}"] = {\n`;
                    map.data.forEach(data => {
                        const row = data.vals;
                        const val = row[map.valCol];
                        if (row[map.noteCol]) {
                            code += `    /** ${row[map.noteCol]} */\n`;
                        }
                        code += `    ${row[map.keyCol]}: ${isNaN(parseInt(val)) ? `"${val}"` : val},\n`;
                    })
                    code += `}\n`;
                    break;
                case "enum":
                    code += "\n";
                    code += `/** ${map.note} */\n`;
                    code += `enum _${map.name} {\n`;
                    map.data.forEach(data => {
                        const row = data.vals;
                        const val = row[map.valCol];
                        if (row[map.noteCol]) {
                            code += `    /** ${row[map.noteCol]} */\n`;
                        }
                        code += `    ${row[map.keyCol]} = ${isNaN(parseInt(val)) ? `"${val}"` : val},\n`;
                    });
                    code += `}\n`;
                    code += `win["${map.name}"] = _${map.name};\n`;
                    break;
            }
        });

        code += "\n";
        code += "declare global {\n";

        this._maps.forEach(map => {

            code += "\n";

            switch (map.mapType) {
                case "map":
                    code += `    /** ${map.note} */\n`;
                    code += `    const ${map.name}: Readonly<typeof _${map.name}>;\n`;
                    break;
                case "enum":
                    code += `    /** ${map.note} */\n`;
                    code += `    const ${map.name}: typeof _${map.name};\n`;
                    break;
                case "const enum":
                    code += `    /** ${map.note} */\n`;
                    code += `    const enum ${map.name} {\n`;
                    map.data.forEach(data => {
                        const row = data.vals;
                        const val = row[map.valCol];
                        if (row[map.noteCol]) {
                            code += `        /** ${row[map.noteCol]} */\n`;
                        }
                        code += `        ${row[map.keyCol]} = ${isNaN(parseInt(val)) ? `"${val}"` : val},\n`;
                    });
                    code += `    }\n`;
                    break;
            }
        });

        this._xlsxs.forEach(xlsx => {

            code += "\n";

            const clsName = this.prefix + xlsx.name;
            if (xlsx.isMisc) {

                code += `    /** ${xlsx.note} */\n`;
                code += `    const ${clsName}: {\n`;
                code += `        /** 表格名称 */\n`;
                code += `        readonly $name: string;\n`;
                xlsx.fields.forEach(field => {
                    const { note, name, type } = field;
                    if (note) {
                        code += `        /** ${note} */\n`;
                    }
                    code += `        readonly ${name}: ${type};\n`;
                });
                code += `    }\n`;

            } else {

                code += `    /** ${xlsx.note} */\n`;
                code += `    class ${clsName} {\n`;
                code += `        /** 表格名称 */\n`;
                code += `        static readonly $name: string;\n`;
                code += `        /** 行数据 */\n`;
                code += `        static readonly rows: readonly ${clsName}[];\n`;
                code += `        /** 通过key值获取行数据 */\n`;
                code += `        static get: (${xlsx.keys.map((key, idx) => `${key}: ${xlsx.keysTypeStr[idx]}`).join(", ")}) => ${clsName};\n`;
                xlsx.fields.forEach(field => {
                    const { note, name, type } = field;
                    if (note) {
                        code += `        /** ${note} */\n`;
                    }
                    code += `        readonly ${name}: ${type};\n`;
                });
                code += `    }\n`;

            }

        });
        code += "}";

        fs.writeFileSync(path.resolve(this.codeOutputDir, `${this.codeName}.ts`), code, { encoding: "utf-8", flag: "w+" });
        console.log(`耗时：${(Date.now() - now) / 1000}s`);
    }
}

/** 断言失败退出程序并输出错误信息 */
function assert(cond: any, msg?: string) {
    if (!cond) {
        console.log("解析错误 === " + msg);
        process.exit();
    }
    return cond;
}

interface IField {
    name: string
    col: number,
    note: string,
    type: string,
    svt: SVT,
    arrVec: number,
    structField: IStructField,
    isKey: boolean,
}

interface IXlsx {
    note: string,
    name: string,
    isMisc: boolean,
    keys: string[];
    keysTypeStr: string[];
    fields: IField[],
    data: { row: number, vals: any }[]
}

interface IMap {
    tableName: string,
    mapType: string,
    note: string,
    name: string,
    noteCol: number,
    keyCol: number,
    valCol: number
    data: { row: number, vals: any }[],
}